C.S.Burner 🪙 ━►🔥GIT Node
Node / reticulum_mirror / MeshChatX / files / meshchatx / src / frontend / components / profile / ProfileIconPage.vue
Displaying Raw • Download
meshchatx/src/frontend/components/profile/ProfileIconPage.vue 65ca9ddc823fcc2c8f8653c553b944b85db0875f (65ca9ddc) Text, 20.09 KB
Ta5d6ff<!-- SPDX-License-Identifier: 0BSD AND MIT -->
Ta5d6ff<template>
Ta5d6ff<div class="flex flex-col flex-1 overflow-hidden min-w-0 dark:bg-zinc-950">
Ta5d6ff<div class="overflow-y-auto">
Ta5d6ff<div class="max-w-4xl mx-auto p-4 space-y-6">
Ta5d6ff<!-- Header with Preview -->
Ta5d6ff<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
Ta5d6ff<div class="p-6 border-b border-gray-200 dark:border-zinc-800">
Ta5d6ff<div class="flex items-center justify-between">
Ta5d6ff<div>
Ta5d6ff<h2 class="text-xl font-bold text-gray-900 dark:text-white">Te6edf3Profile Te6edf3Icon Te6edf3CustomizerTa5d6ff</h2>
Ta5d6ff<p class="text-sm text-gray-500 dark:text-zinc-400 mt-1">
Te6edf3Customize Te6edf3your Te6edf3profile Te6edf3icon Te6edf3that Te6edf3appears Tffa657in Tffa657all Te6edf3your Te6edf3messages
Ta5d6ff</p>
Ta5d6ff</div>
Ta5d6ff<div class="flex items-center gap-3">
Ta5d6ff<button
type="button"
:disabled="!hasChanges || isSaving"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
:class="
hasChanges && !isSaving
? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:border-blue-500 dark:hover:bg-blue-600'
: 'bg-gray-100 text-gray-700 border-gray-300 dark:bg-zinc-800 dark:text-zinc-300 dark:border-zinc-700'
"
@click="saveChanges"
>
<MaterialDesignIcon
v-if="isSaving"
icon-name="refresh"
class="size-4 animate-spin"
/>
Ta5d6ff<MaterialDesignIcon v-else icon-name="content-save" class="size-4" />
{{ Te6edf3isSaving ? Ta5d6ff"Saving..." : Ta5d6ff"Save" }}
Ta5d6ff</button>
Ta5d6ff<button
type="button"
:disabled="!hasChanges || isSaving"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
@click="resetChanges"
>
<MaterialDesignIcon icon-name="refresh" class="size-4" />
Te6edf3Reset
Ta5d6ff</button>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<div class="p-6">
Ta5d6ff<div class="flex flex-col items-center justify-center space-y-4">
Ta5d6ff<div class="text-sm font-medium text-gray-700 dark:text-zinc-300">Te6edf3PreviewTa5d6ff</div>
Ta5d6ff<div class="p-8 bg-gray-50 dark:bg-zinc-800 rounded-2xl">
Ta5d6ff<LxmfUserIcon
:key="iconName + iconForegroundColour + iconBackgroundColour"
:icon-name="iconName"
:icon-foreground-colour="iconForegroundColour"
:icon-background-colour="iconBackgroundColour"
icon-class="size-24"
/>
Ta5d6ff</div>
Ta5d6ff<div class="text-xs text-gray-500 dark:text-zinc-400 text-center max-w-md">
Te6edf3This Tff7b72is Tffa657how Te6edf3your Te6edf3icon Te6edf3will Te6edf3appear Tffa657to Te6edf3others Tff7b72when Te6edf3you Tffa657send Te6edf3messages
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<!-- Color Selection -->
Ta5d6ff<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
Ta5d6ff<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
Ta5d6ff<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Te6edf3ColorsTa5d6ff</h3>
Ta5d6ff</div>
Ta5d6ff<div class="p-4 space-y-4">
Ta5d6ff<div class="flex items-center justify-between gap-4">
Ta5d6ff<div class="flex-1">
Ta5d6ff<label class="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
Te6edf3Background Te6edf3Color
Ta5d6ff</label>
Ta5d6ff<div class="flex items-center gap-3">
Ta5d6ff<ColourPickerDropdown v-model:colour="iconBackgroundColour" />
Ta5d6ff<div class="flex-1">
Ta5d6ff<input
v-model="iconBackgroundColour"
type="text"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="#e5e7eb"
/>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<div class="flex items-center justify-between gap-4">
Ta5d6ff<div class="flex-1">
Ta5d6ff<label class="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
Te6edf3Icon Te6edf3Color
Ta5d6ff</label>
Ta5d6ff<div class="flex items-center gap-3">
Ta5d6ff<ColourPickerDropdown v-model:colour="iconForegroundColour" />
Ta5d6ff<div class="flex-1">
Ta5d6ff<input
v-model="iconForegroundColour"
type="text"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="#6b7280"
/>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<!-- Icon Selection -->
Ta5d6ff<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
Ta5d6ff<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Te6edf3IconTa5d6ff</h3>
Ta5d6ff</div>
Ta5d6ff<div class="p-4 space-y-4">
Ta5d6ff<div class="relative">
Ta5d6ff<input
v-model="search"
type="text"
:placeholder="`Search ${iconNames.length} icons...`"
class="w-full px-4 py-3 text-sm border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 placeholder-gray-400 dark:placeholder-zinc-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
Ta5d6ff<MaterialDesignIcon
icon-name="magnify"
class="absolute right-3 top-1/2 -translate-y-1/2 size-5 text-gray-400 dark:text-zinc-500 pointer-events-none"
/>
Ta5d6ff</div>
Ta5d6ff<div
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 max-h-[500px] overflow-y-auto p-1"
>
<div
v-for="mdiIconName of searchedIconNames"
:key="mdiIconName"
class="flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer transition-all hover:bg-gray-50 dark:hover:bg-zinc-800 hover:border-blue-500 dark:hover:border-blue-500"
:class="
iconName === mdiIconName
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-zinc-700'
"
@click="onIconClick(mdiIconName)"
>
<LxmfUserIcon
:key="
mdiIconName +
(iconName === mdiIconName ? iconForegroundColour + iconBackgroundColour : '')
"
:icon-name="mdiIconName"
:icon-foreground-colour="
iconName === mdiIconName ? iconForegroundColour : '#6b7280'
"
:icon-background-colour="
iconName === mdiIconName ? iconBackgroundColour : '#e5e7eb'
"
icon-class="size-12"
/>
Ta5d6ff<div
class="mt-2 text-xs text-center text-gray-600 dark:text-zinc-400 truncate w-full"
:title="mdiIconName"
>
{{ mdiIconName }}
</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<div
v-if="searchedIconNames.length === 0"
class="text-center py-8 text-sm text-gray-500 dark:text-zinc-400"
>
No icons match your search.
</div>
Ta5d6ff<div
v-if="searchedIconNames.length === maxSearchResults"
class="text-center py-2 text-xs text-gray-500 dark:text-zinc-400"
>
Showing first {{ maxSearchResults }} results. Refine your search to see more.
</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff<!-- Remove Icon Section -->
Ta5d6ff<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
Ta5d6ff<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Te6edf3Remove Te6edf3IconTa5d6ff</h3>
Ta5d6ff</div>
Ta5d6ff<div class="p-4">
Ta5d6ff<p class="text-sm text-gray-600 dark:text-zinc-400 mb-4">
Te6edf3Remove Te6edf3your Te6edf3profile Te6edf3icon. Te6edf3Anyone Te6edf3who Tff7b72has Te6edf3already Te6edf3received Te6edf3it Te6edf3will Te6edf3continue Tffa657to Te6edf3see Te6edf3it Tff7b72until
Te6edf3you Tffa657send Te6edf3them Te6edf3a Tffa657new Te6edf3icon.
Ta5d6ff</p>
Ta5d6ff<button
type="button"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-red-300 dark:border-red-800 bg-white dark:bg-zinc-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
@click="removeProfileIcon"
>
<MaterialDesignIcon icon-name="delete-outline" class="size-4" />
Te6edf3Remove Te6edf3Icon
Ta5d6ff</button>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</div>
Ta5d6ff</template>
Ta5d6ff<script>
Tff7b72import * Te6edf3as Te6edf3mdi Tffa657from Ta5d6ff"@mdi/js";
Tff7b72import Te6edf3LxmfUserIcon Tffa657from Ta5d6ff"../LxmfUserIcon.vue";
Tff7b72import Te6edf3ToastUtils Tffa657from Ta5d6ff"../../js/ToastUtils";
Tff7b72import Te6edf3ColourPickerDropdown Tffa657from Ta5d6ff"../ColourPickerDropdown.vue";
Tff7b72import Te6edf3MaterialDesignIcon Tffa657from Ta5d6ff"../MaterialDesignIcon.vue";
Tff7b72import Te6edf3GlobalEmitter Tffa657from Ta5d6ff"../../js/GlobalEmitter";
Tff7b72import { Te6edf3mergeGlobalConfig } Tffa657from Ta5d6ff"../../js/GlobalState";
Tff7b72export Tff7b72default {
Te6edf3name: Ta5d6ff"ProfileIconPage",
Te6edf3components: {
Te6edf3ColourPickerDropdown,
Te6edf3LxmfUserIcon,
Te6edf3MaterialDesignIcon,
},
Te6edf3data() {
Tff7b72return {
Te6edf3config: Te6edf3null,
Te6edf3iconName: Te6edf3null,
Te6edf3iconForegroundColour: Te6edf3null,
Te6edf3iconBackgroundColour: Te6edf3null,
Te6edf3originalIconName: Te6edf3null,
Te6edf3originalIconForegroundColour: Te6edf3null,
Te6edf3originalIconBackgroundColour: Te6edf3null,
Te6edf3search: Ta5d6ff"",
Te6edf3maxSearchResults: T79c0ff200,
Te6edf3iconNames: [],
Te6edf3isSaving: Te6edf3false,
Te6edf3autoSaveTimeout: Te6edf3null,
};
},
Te6edf3computed: {
Te6edf3searchedIconNames() {
Te6edf3const Te6edf3searchLower = Te6edf3this.Te6edf3search.Te6edf3toLowerCase();
Tff7b72return Te6edf3this.Te6edf3iconNames
.Te6edf3filter((Te6edf3iconName) => {
Tff7b72return Te6edf3iconName.Te6edf3toLowerCase().Te6edf3includes(Te6edf3searchLower);
})
.Te6edf3slice(T79c0ff0, Te6edf3this.Te6edf3maxSearchResults);
},
Te6edf3hasChanges() {
Tff7b72return (
Te6edf3this.Te6edf3iconName !== Te6edf3this.Te6edf3originalIconName ||
Te6edf3this.Te6edf3iconForegroundColour !== Te6edf3this.Te6edf3originalIconForegroundColour ||
Te6edf3this.Te6edf3iconBackgroundColour !== Te6edf3this.Te6edf3originalIconBackgroundColour
);
},
},
Te6edf3watch: {
Te6edf3config: {
Te6edf3handler() {
Tff7b72if (Te6edf3this.Tffa657config) {
Te6edf3this.Te6edf3iconName = Te6edf3this.Tffa657config.Te6edf3lxmf_user_icon_name || Te6edf3null;
Te6edf3this.Te6edf3iconForegroundColour = Te6edf3this.Tffa657config.Te6edf3lxmf_user_icon_foreground_colour || Ta5d6ff"#6b7280";
Te6edf3this.Te6edf3iconBackgroundColour = Te6edf3this.Tffa657config.Te6edf3lxmf_user_icon_background_colour || Ta5d6ff"#e5e7eb";
Te6edf3this.Te6edf3saveOriginalValues();
}
},
Te6edf3immediate: Te6edf3true,
},
Te6edf3iconForegroundColour() {
Te6edf3this.Te6edf3debouncedAutoSave();
},
Te6edf3iconBackgroundColour() {
Te6edf3this.Te6edf3debouncedAutoSave();
},
Te6edf3iconName() {
Te6edf3this.Te6edf3debouncedAutoSave();
},
},
Te6edf3mounted() {
Te6edf3this.Te6edf3getConfig();
Te6edf3this.Te6edf3iconNames = Te6edf3Object.Tffa657keys(Te6edf3mdi).Te6edf3map((Te6edf3mdiIcon) => {
Tff7b72return Te6edf3mdiIcon
.Te6edf3replace(Ta5d6ff/^mdi/, Ta5d6ff"")
.Te6edf3replace(Ta5d6ff/([a-z])([A-Z])/Te6edf3g, Ta5d6ff"$1-$2")
.Te6edf3toLowerCase();
});
},
Te6edf3beforeUnmount() {
Tff7b72if (Te6edf3this.Te6edf3autoSaveTimeout) {
Te6edf3clearTimeout(Te6edf3this.Te6edf3autoSaveTimeout);
}
},
Te6edf3methods: {
Te6edf3saveOriginalValues() {
Te6edf3this.Te6edf3originalIconName = Te6edf3this.Te6edf3iconName;
Te6edf3this.Te6edf3originalIconForegroundColour = Te6edf3this.Te6edf3iconForegroundColour;
Te6edf3this.Te6edf3originalIconBackgroundColour = Te6edf3this.Te6edf3iconBackgroundColour;
},
Te6edf3debouncedAutoSave() {
Tff7b72if (Te6edf3this.Te6edf3autoSaveTimeout) {
Te6edf3clearTimeout(Te6edf3this.Te6edf3autoSaveTimeout);
}
Te6edf3this.Te6edf3autoSaveTimeout = Te6edf3setTimeout(() => {
Tff7b72if (Te6edf3this.Te6edf3hasChanges && Te6edf3this.Te6edf3iconName && Te6edf3this.Te6edf3iconForegroundColour && Te6edf3this.Te6edf3iconBackgroundColour) {
Te6edf3this.Te6edf3saveChanges(Te6edf3true);
}
}, T79c0ff1000);
},
Te6edf3async Te6edf3getConfig() {
Tff7b72try {
Te6edf3const Te6edf3response = Tffa657await Te6edf3window.Te6edf3api.Tffa657get(Ta5d6ff"/api/v1/config");
Te6edf3const Tffa657next = Te6edf3response.Te6edf3data?.Tffa657config;
Tff7b72if (Tffa657next && Te6edf3typeof Tffa657next === Ta5d6ff"object") {
Te6edf3this.Tffa657config = Tffa657next;
Te6edf3mergeGlobalConfig(Tffa657next);
}
} Te6edf3catch (Tffa657e) {
Te6edf3ToastUtils.Te6edf3error(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.failed_load_config"));
Te6edf3console.Te6edf3error(Tffa657e);
}
},
Te6edf3async Te6edf3updateConfig(Tffa657config, Te6edf3silent = Te6edf3false) {
Tff7b72try {
Te6edf3const Te6edf3response = Tffa657await Te6edf3window.Te6edf3api.Te6edf3patch(Ta5d6ff"/api/v1/config", Tffa657config);
Te6edf3const Tffa657next = Te6edf3response.Te6edf3data?.Tffa657config;
Tff7b72if (!Tffa657next || Te6edf3typeof Tffa657next !== Ta5d6ff"object") {
Tff7b72return Te6edf3false;
}
Te6edf3mergeGlobalConfig(Tffa657next);
Te6edf3this.Tffa657config = Tffa657next;
Te6edf3GlobalEmitter.Tffa657emit(Ta5d6ff"config-updated", Tffa657next);
Te6edf3this.Te6edf3saveOriginalValues();
Tff7b72if (!Te6edf3silent) {
Te6edf3ToastUtils.Te6edf3success(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.profile_icon_saved"));
}
Tff7b72return Te6edf3true;
} Te6edf3catch (Tffa657e) {
Tff7b72if (!Te6edf3silent) {
Te6edf3ToastUtils.Te6edf3error(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.failed_save_profile_icon"));
}
Te6edf3console.Te6edf3error(Tffa657e);
Tff7b72return Te6edf3false;
}
},
Te6edf3async Te6edf3saveChanges(Te6edf3silent = Te6edf3false) {
Tff7b72if (!Te6edf3this.Te6edf3hasChanges) {
Tff7b72return;
}
Tff7b72if (!Te6edf3this.Te6edf3iconForegroundColour || !Te6edf3this.Te6edf3iconBackgroundColour) {
Te6edf3ToastUtils.Te6edf3warning(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.select_colors_warning"));
Tff7b72return;
}
Tff7b72if (!Te6edf3this.Te6edf3iconName) {
Te6edf3ToastUtils.Te6edf3warning(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.select_icon_warning"));
Tff7b72return;
}
Te6edf3this.Te6edf3isSaving = Te6edf3true;
Tff7b72try {
Te6edf3const Te6edf3success = Tffa657await Te6edf3this.Te6edf3updateConfig(
{
Te6edf3lxmf_user_icon_name: Te6edf3this.Te6edf3iconName,
Te6edf3lxmf_user_icon_foreground_colour: Te6edf3this.Te6edf3iconForegroundColour,
Te6edf3lxmf_user_icon_background_colour: Te6edf3this.Te6edf3iconBackgroundColour,
},
Te6edf3silent
);
Tff7b72if (Te6edf3success && !Te6edf3silent) {
Te6edf3ToastUtils.Te6edf3success(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.profile_icon_saved"));
}
} Te6edf3finally {
Te6edf3this.Te6edf3isSaving = Te6edf3false;
}
},
Te6edf3resetChanges() {
Tff7b72if (!Te6edf3this.Te6edf3hasChanges) {
Tff7b72return;
}
Te6edf3this.Te6edf3iconName = Te6edf3this.Te6edf3originalIconName;
Te6edf3this.Te6edf3iconForegroundColour = Te6edf3this.Te6edf3originalIconForegroundColour;
Te6edf3this.Te6edf3iconBackgroundColour = Te6edf3this.Te6edf3originalIconBackgroundColour;
Te6edf3ToastUtils.Te6edf3info(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.changes_reset"));
},
Te6edf3onIconClick(Te6edf3iconName) {
Te6edf3this.Te6edf3iconName = Te6edf3iconName;
},
Te6edf3async Te6edf3removeProfileIcon() {
Te6edf3this.Te6edf3isSaving = Te6edf3true;
Tff7b72try {
Te6edf3const Te6edf3success = Tffa657await Te6edf3this.Te6edf3updateConfig({
Te6edf3lxmf_user_icon_name: Te6edf3null,
Te6edf3lxmf_user_icon_foreground_colour: Te6edf3null,
Te6edf3lxmf_user_icon_background_colour: Te6edf3null,
});
Tff7b72if (Te6edf3success) {
Te6edf3ToastUtils.Te6edf3success(Te6edf3this.Te6edf3$t(Ta5d6ff"messages.profile_icon_removed"));
}
} Te6edf3finally {
Te6edf3this.Te6edf3isSaving = Te6edf3false;
}
},
},
};
Ta5d6ff</script>
Served by rngit 1.5.2 - Generated in 0.09s